<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webaudio - timing</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
	</head>
<body>

	<div id="overlay">
		<button id="startButton">Play</button>
	</div>
	<div id="container"></div>
	<div id="info">
		<a href="https://threejs.org" target="_blank" rel="noopener noreferrer">three.js</a> webaudio - timing<br/>
		sound effect by <a href="https://freesound.org/people/michorvath/sounds/269718/" target="_blank" rel="noopener noreferrer">michorvath</a>
	</div>

	<script type="module">
		import * as THREE from '../build/three.module.js';

		import { OrbitControls } from './jsm/controls/OrbitControls.js';

		let scene, camera, renderer, clock;

		const objects = [];

		const speed = 2.5;
		const height = 3;
		const offset = 0.5;

		const startButton = document.getElementById( 'startButton' );
		startButton.addEventListener( 'click', init );

		function init() {

			const overlay = document.getElementById( 'overlay' );
			overlay.remove();

			const container = document.getElementById( 'container' );

			scene = new THREE.Scene();

			clock = new THREE.Clock();

			//

			camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
			camera.position.set( 7, 3, 7 );

			// lights

			const ambientLight = new THREE.AmbientLight( 0xcccccc, 0.4 );
			scene.add( ambientLight );

			const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.7 );
			directionalLight.position.set( 0, 5, 5 );
			scene.add( directionalLight );

			const d = 5;
			directionalLight.castShadow = true;
			directionalLight.shadow.camera.left = - d;
			directionalLight.shadow.camera.right = d;
			directionalLight.shadow.camera.top = d;
			directionalLight.shadow.camera.bottom = - d;

			directionalLight.shadow.camera.near = 1;
			directionalLight.shadow.camera.far = 20;

			directionalLight.shadow.mapSize.x = 1024;
			directionalLight.shadow.mapSize.y = 1024;

			// audio

			const audioLoader = new THREE.AudioLoader();

			const listener = new THREE.AudioListener();
			camera.add( listener );

			// floor

			const floorGeometry = new THREE.PlaneGeometry( 10, 10 );
			const floorMaterial = new THREE.MeshLambertMaterial( { color: 0x4676b6 } );

			const floor = new THREE.Mesh( floorGeometry, floorMaterial );
			floor.rotation.x = Math.PI * - 0.5;
			floor.receiveShadow = true;
			scene.add( floor );

			// objects

			const count = 5;
			const radius = 3;

			const ballGeometry = new THREE.SphereGeometry( 0.3, 32, 16 );
			ballGeometry.translate( 0, 0.3, 0 );
			const ballMaterial = new THREE.MeshLambertMaterial( { color: 0xcccccc } );

			// create objects when audio buffer is loaded

			audioLoader.load( 'sounds/ping_pong.mp3', function ( buffer ) {

				for ( let i = 0; i < count; i ++ ) {

					const s = i / count * Math.PI * 2;

					const ball = new THREE.Mesh( ballGeometry, ballMaterial );
					ball.castShadow = true;
					ball.userData.down = false;

					ball.position.x = radius * Math.cos( s );
					ball.position.z = radius * Math.sin( s );

					const audio = new THREE.PositionalAudio( listener );
					audio.setBuffer( buffer );
					ball.add( audio );

					scene.add( ball );
					objects.push( ball );

				}

				animate();

			} );

			//

			renderer = new THREE.WebGLRenderer( { antialias: true } );
			renderer.shadowMap.enabled = true;
			renderer.setSize( window.innerWidth, window.innerHeight );
			renderer.setClearColor( 0x000000 );
			renderer.setPixelRatio( window.devicePixelRatio );
			container.appendChild( renderer.domElement );

			//

			const controls = new OrbitControls( camera, renderer.domElement );
			controls.minDistance = 1;
			controls.maxDistance = 25;

			//

			window.addEventListener( 'resize', onWindowResize );

		}

		function onWindowResize() {

			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();

			renderer.setSize( window.innerWidth, window.innerHeight );

		}

		function animate() {

			requestAnimationFrame( animate );

			render();

		}

		function render() {

			const time = clock.getElapsedTime();

			for ( let i = 0; i < objects.length; i ++ ) {

				const ball = objects[ i ];

				const previousHeight = ball.position.y;
				ball.position.y = Math.abs( Math.sin( i * offset + ( time * speed ) ) * height );

				if ( ball.position.y < previousHeight ) {

					ball.userData.down = true;

				} else {

					if ( ball.userData.down === true ) {

						// ball changed direction from down to up

						const audio = ball.children[ 0 ];
						audio.play(); // play audio with perfect timing when ball hits the surface
						ball.userData.down = false;

					}

				}

			}

			renderer.render( scene, camera );

		}

	</script>

</body>
</html>
